/*-------------------------------------------------------------------------
 *
 * vacuum.c
 *	  PostgreSQL 垃圾清理工具。
 *
 * 此文件包括（a）VACUUM 和 ANALYZE 命令的控制和调度代码，（b）计算各种垃圾清理阈值的代码，以及 (c) 索引垃圾清理代码。
 *
 * 堆 AM 的 VACUUM 在 vacuumlazy.c 中实现，并行垃圾清理在 vacuumparallel.c 中实现，ANALYZE 在 analyze.c 中实现，VACUUM FULL 是 CLUSTER 的变体，在 cluster.c 中处理。
 *
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 *
 * IDENTIFICATION
 *	  src/backend/commands/vacuum.c
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include <math.h>

#include "access/clog.h"
#include "access/commit_ts.h"
#include "access/genam.h"
#include "access/heapam.h"
#include "access/htup_details.h"
#include "access/multixact.h"
#include "access/tableam.h"
#include "access/transam.h"
#include "access/xact.h"
#include "catalog/namespace.h"
#include "catalog/index.h"
#include "catalog/pg_database.h"
#include "catalog/pg_inherits.h"
#include "catalog/pg_namespace.h"
#include "commands/cluster.h"
#include "commands/defrem.h"
#include "commands/vacuum.h"
#include "miscadmin.h"
#include "nodes/makefuncs.h"
#include "pgstat.h"
#include "postmaster/autovacuum.h"
#include "postmaster/bgworker_internals.h"
#include "storage/bufmgr.h"
#include "storage/lmgr.h"
#include "storage/pmsignal.h"
#include "storage/proc.h"
#include "storage/procarray.h"
#include "utils/acl.h"
#include "utils/fmgroids.h"
#include "utils/guc.h"
#include "utils/memutils.h"
#include "utils/pg_rusage.h"
#include "utils/snapmgr.h"
#include "utils/syscache.h"


/*
 * GUC参数
 */
int			vacuum_freeze_min_age;
int			vacuum_freeze_table_age;
int			vacuum_multixact_freeze_min_age;
int			vacuum_multixact_freeze_table_age;
int			vacuum_failsafe_age;
int			vacuum_multixact_failsafe_age;


/* 一些看起来不值得作为参数传递的变量 */
static MemoryContext vac_context = NULL;
static BufferAccessStrategy vac_strategy;


/*
 * 基于成本的并行清理的变量。有关其工作原理，请参见 compute_parallel_delay 顶部的注释。
 */
pg_atomic_uint32 *VacuumSharedCostBalance = NULL;
pg_atomic_uint32 *VacuumActiveNWorkers = NULL;
int			VacuumCostBalanceLocal = 0;

/* 非导出函数原型 */
static List *fc_expand_vacuum_rel(VacuumRelation *fc_vrel, int fc_options);
static List *fc_get_all_vacuum_rels(int fc_options);
static void fc_vac_truncate_clog(TransactionId fc_frozenXID,
							  MultiXactId fc_minMulti,
							  TransactionId fc_lastSaneFrozenXid,
							  MultiXactId fc_lastSaneMinMulti);
static bool fc_vacuum_rel(Oid fc_relid, RangeVar *fc_relation, VacuumParams *fc_params);
static double fc_compute_parallel_delay(void);
static VacOptValue fc_get_vacoptval_from_boolean(DefElem *fc_def);
static bool fc_vac_tid_reaped(ItemPointer fc_itemptr, void *fc_state);
static int	fc_vac_cmp_itemptr(const void *fc_left, const void *fc_right);

/*
 * 手动 VACUUM 和 ANALYZE 命令的主要入口点
 *
 * 这主要是即将发生的 real operations 的准备包装。
 */
void ExecVacuum(ParseState *fc_pstate, VacuumStmt *fc_vacstmt, bool fc_isTopLevel)
{
	VacuumParams fc_params;
	bool		fc_verbose = false;
	bool		fc_skip_locked = false;
	bool		fc_analyze = false;
	bool		fc_freeze = false;
	bool		fc_full = false;
	bool		fc_disable_page_skipping = false;
	bool		fc_process_toast = true;
	ListCell   *fc_lc;

	/* index_cleanup 和截断值暂时未指定 */
	fc_params.index_cleanup = VACOPTVALUE_UNSPECIFIED;
	fc_params.truncate = VACOPTVALUE_UNSPECIFIED;

	/* 默认情况下启用并行清理 */
	fc_params.nworkers = 0;

	/* 解析选项列表 */
	foreach(fc_lc, fc_vacstmt->options)
	{
		DefElem    *fc_opt = (DefElem *) lfirst(fc_lc);

		/* 解析 VACUUM 和 ANALYZE 的通用选项 */
		if (strcmp(fc_opt->defname, "verbose") == 0)
			fc_verbose = defGetBoolean(fc_opt);
		else if (strcmp(fc_opt->defname, "skip_locked") == 0)
			fc_skip_locked = defGetBoolean(fc_opt);
		else if (!fc_vacstmt->is_vacuumcmd)
			ereport(ERROR,
					(errcode(ERRCODE_SYNTAX_ERROR),
					 errmsg("unrecognized ANALYZE option \"%s\"", fc_opt->defname),
					 parser_errposition(fc_pstate, fc_opt->location)));

		/* 解析 VACUUM 可用的选项 */
		else if (strcmp(fc_opt->defname, "analyze") == 0)
			fc_analyze = defGetBoolean(fc_opt);
		else if (strcmp(fc_opt->defname, "freeze") == 0)
			fc_freeze = defGetBoolean(fc_opt);
		else if (strcmp(fc_opt->defname, "full") == 0)
			fc_full = defGetBoolean(fc_opt);
		else if (strcmp(fc_opt->defname, "disable_page_skipping") == 0)
			fc_disable_page_skipping = defGetBoolean(fc_opt);
		else if (strcmp(fc_opt->defname, "index_cleanup") == 0)
		{
			/* 将没有字符串解释为默认值，即 'auto' */
			if (!fc_opt->arg)
				fc_params.index_cleanup = VACOPTVALUE_AUTO;
			else
			{
				char	   *fc_sval = defGetString(fc_opt);

				/* 尝试匹配 'auto' 字符串，或回退到布尔值 */
				if (pg_strcasecmp(fc_sval, "auto") == 0)
					fc_params.index_cleanup = VACOPTVALUE_AUTO;
				else
					fc_params.index_cleanup = fc_get_vacoptval_from_boolean(fc_opt);
			}
		}
		else if (strcmp(fc_opt->defname, "process_toast") == 0)
			fc_process_toast = defGetBoolean(fc_opt);
		else if (strcmp(fc_opt->defname, "truncate") == 0)
			fc_params.truncate = fc_get_vacoptval_from_boolean(fc_opt);
		else if (strcmp(fc_opt->defname, "parallel") == 0)
		{
			if (fc_opt->arg == NULL)
			{
				ereport(ERROR,
						(errcode(ERRCODE_SYNTAX_ERROR),
						 errmsg("parallel option requires a value between 0 and %d",
								MAX_PARALLEL_WORKER_LIMIT),
						 parser_errposition(fc_pstate, fc_opt->location)));
			}
			else
			{
				int			fc_nworkers;

				fc_nworkers = defGetInt32(fc_opt);
				if (fc_nworkers < 0 || fc_nworkers > MAX_PARALLEL_WORKER_LIMIT)
					ereport(ERROR,
							(errcode(ERRCODE_SYNTAX_ERROR),
							 errmsg("parallel workers for vacuum must be between 0 and %d",
									MAX_PARALLEL_WORKER_LIMIT),
							 parser_errposition(fc_pstate, fc_opt->location)));

				/*
				 * 如果用户指定的并行度为零，则禁用并行清理。
				 */
				if (fc_nworkers == 0)
					fc_params.nworkers = -1;
				else
					fc_params.nworkers = fc_nworkers;
			}
		}
		else
			ereport(ERROR,
					(errcode(ERRCODE_SYNTAX_ERROR),
					 errmsg("unrecognized VACUUM option \"%s\"", fc_opt->defname),
					 parser_errposition(fc_pstate, fc_opt->location)));
	}

	/* 设置清理选项 */
	fc_params.options =
		(fc_vacstmt->is_vacuumcmd ? VACOPT_VACUUM : VACOPT_ANALYZE) |
		(fc_verbose ? VACOPT_VERBOSE : 0) |
		(fc_skip_locked ? VACOPT_SKIP_LOCKED : 0) |
		(fc_analyze ? VACOPT_ANALYZE : 0) |
		(fc_freeze ? VACOPT_FREEZE : 0) |
		(fc_full ? VACOPT_FULL : 0) |
		(fc_disable_page_skipping ? VACOPT_DISABLE_PAGE_SKIPPING : 0) |
		(fc_process_toast ? VACOPT_PROCESS_TOAST : 0);

	/* 对选项的有效性检查 */
	Assert(fc_params.options & (VACOPT_VACUUM | VACOPT_ANALYZE));
	Assert((fc_params.options & VACOPT_VACUUM) ||
		   !(fc_params.options & (VACOPT_FULL | VACOPT_FREEZE)));

	if ((fc_params.options & VACOPT_FULL) && fc_params.nworkers > 0)
		ereport(ERROR,
				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
				 errmsg("VACUUM FULL cannot be performed in parallel")));

	/*
	 * 如果存在任何列列表，请确保指定 VACOPT_ANALYZE。
	 */
	if (!(fc_params.options & VACOPT_ANALYZE))
	{
		ListCell   *fc_lc;

		foreach(fc_lc, fc_vacstmt->rels)
		{
			VacuumRelation *fc_vrel = lfirst_node(VacuumRelation, fc_lc);

			if (fc_vrel->va_cols != NIL)
				ereport(ERROR,
						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
						 errmsg("ANALYZE option must be specified when a column list is provided")));
		}
	}

	/*
	 * 如果给定 FREEZE 选项，则所有冻结年龄为零；否则传递为 -1，表示使用默认值。
	 */
	if (fc_params.options & VACOPT_FREEZE)
	{
		fc_params.freeze_min_age = 0;
		fc_params.freeze_table_age = 0;
		fc_params.multixact_freeze_min_age = 0;
		fc_params.multixact_freeze_table_age = 0;
	}
	else
	{
		fc_params.freeze_min_age = -1;
		fc_params.freeze_table_age = -1;
		fc_params.multixact_freeze_min_age = -1;
		fc_params.multixact_freeze_table_age = -1;
	}

	/* 用户调用的清理从不“用于环绕” */
	fc_params.is_wraparound = false;

	/* 用户调用的清理使用 VACOPT_VERBOSE 而不是 log_min_duration */
	fc_params.log_min_duration = -1;

	/* 现在通过通用例程 */
	vacuum(fc_vacstmt->rels, &fc_params, NULL, fc_isTopLevel);
}

/*
 * VACUUM 和 ANALYZE 命令的内部入口点。
 *
 * 如果 relations 不是 NIL，那么它是要处理的 VacuumRelation 列表；否则，
 * 我们处理数据库中的所有相关表。对于每个 VacuumRelation，
 * 如果提供了有效的 OID，那么将处理具有该 OID 的表；
 * 否则，VacuumRelation 的 RangeVar 指示要处理的内容。
 *
 * params 包含一组可以用来定制行为的参数。
 *
 * bstrategy 通常给定为 NULL，但在自动清理中，可以传入以跨多个 vacuum() 调用使用相同的缓冲区策略对象。
 *
 * isTopLevel 应该从 ProcessUtility 传递下来。
 *
 * 确保所有参数在一个内存上下文中分配，该上下文在事务提交时不会消失，是调用者的责任。
 */
void vacuum(List *fc_relations, VacuumParams *fc_params,
	   BufferAccessStrategy fc_bstrategy, bool fc_isTopLevel)
{
	static bool fc_in_vacuum = false;

	const char *fc_stmttype;
	volatile bool fc_in_outer_xact,
				fc_use_own_xacts;

	Assert(fc_params != NULL);

	fc_stmttype = (fc_params->options & VACOPT_VACUUM) ? "VACUUM" : "ANALYZE";

	/*
	 * 我们无法在用户事务块内运行 VACUUM；如果我们在事务中，
	 * 则我们的提交和开始事务命令调用将不会产生预期效果！还有许多其他微妙的
	 * 依赖关系也与此有关。
	 *
	 * ANALYZE（不带 VACUUM）可以以任何方式运行。
	 */
	if (fc_params->options & VACOPT_VACUUM)
	{
		PreventInTransactionBlock(fc_isTopLevel, fc_stmttype);
		fc_in_outer_xact = false;
	}
	else
		fc_in_outer_xact = IsInTransactionBlock(fc_isTopLevel);

	/*
	 * 由于静态变量 vac_context、anl_context 和 vac_strategy，
	 * vacuum() 不是可重入的。这一点在 VACUUM FULL 或 ANALYZE
	 * 调用一个敌对的索引表达式时很重要，该表达式本身又调用 ANALYZE。
	 */
	if (fc_in_vacuum)
		ereport(ERROR,
				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
				 errmsg("%s cannot be executed from VACUUM or ANALYZE",
						fc_stmttype)));

	/*
	 * 有效性检查 DISABLE_PAGE_SKIPPING 选项。
	 */
	if ((fc_params->options & VACOPT_FULL) != 0 &&
		(fc_params->options & VACOPT_DISABLE_PAGE_SKIPPING) != 0)
		ereport(ERROR,
				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
				 errmsg("VACUUM option DISABLE_PAGE_SKIPPING cannot be used with FULL")));

	/* PROCESS_TOAST 的有效性检查 */
	if ((fc_params->options & VACOPT_FULL) != 0 &&
		(fc_params->options & VACOPT_PROCESS_TOAST) == 0)
		ereport(ERROR,
				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
				 errmsg("PROCESS_TOAST required with VACUUM FULL")));

	/*
	 * 为跨事务存储创建特殊内存上下文。
	 *
	 * 由于这是PortalContext的子上下文，即使出现错误，它最终也会消失；
	 * 因此不需要特殊的中止清理逻辑。
	 */
	vac_context = AllocSetContextCreate(PortalContext,
										"Vacuum",
										ALLOCSET_DEFAULT_SIZES);

	/*
	 * 如果调用者没有给我们缓冲策略对象，则在跨事务内存上下文中创建一个。
	 */
	if (fc_bstrategy == NULL)
	{
		MemoryContext fc_old_context = MemoryContextSwitchTo(vac_context);

		fc_bstrategy = GetAccessStrategy(BAS_VACUUM);
		MemoryContextSwitchTo(fc_old_context);
	}
	vac_strategy = fc_bstrategy;

	/*
	 * 构建要处理的关系列表，将任何新数据放入vac_context以进行妥善保管。
	 */
	if (fc_relations != NIL)
	{
		List	   *fc_newrels = NIL;
		ListCell   *fc_lc;

		foreach(fc_lc, fc_relations)
		{
			VacuumRelation *fc_vrel = lfirst_node(VacuumRelation, fc_lc);
			List	   *fc_sublist;
			MemoryContext fc_old_context;

			fc_sublist = fc_expand_vacuum_rel(fc_vrel, fc_params->options);
			fc_old_context = MemoryContextSwitchTo(vac_context);
			fc_newrels = list_concat(fc_newrels, fc_sublist);
			MemoryContextSwitchTo(fc_old_context);
		}
		fc_relations = fc_newrels;
	}
	else
		fc_relations = fc_get_all_vacuum_rels(fc_params->options);

	/*
	 * 决定我们是否需要启动/提交自己的事务。
	 *
	 * 对于VACUUM（有或没有ANALYZE）：始终这样做，以便我们尽快释放锁。
	 * （我们可能能够在一次表VACUUM中使用外部事务，但处理TOAST表将会很麻烦。）
	 *
	 * 对于ANALYZE（没有VACUUM）：如果在事务块内，我们不能启动/提交自己的事务。
	 * 此外，如果仅处理一个关系，也没有必要这样做。对于多个关系，当不在事务块内，并且
	 * 也在自动清理工作者中时，使用自己的事务，以便我们可以更早地释放锁。
	 */
	if (fc_params->options & VACOPT_VACUUM)
		fc_use_own_xacts = true;
	else
	{
		Assert(fc_params->options & VACOPT_ANALYZE);
		if (IsAutoVacuumWorkerProcess())
			fc_use_own_xacts = true;
		else if (fc_in_outer_xact)
			fc_use_own_xacts = false;
		else if (list_length(fc_relations) > 1)
			fc_use_own_xacts = true;
		else
			fc_use_own_xacts = false;
	}

	/*
	 * vacuum_rel期望在没有活动事务的情况下被调用；它将启动并提交自己的事务。
	 * 但我们是由SQL命令调用的，因此我们已经在一个事务内执行。
	 * 我们在这里提交在PostgresMain()中启动的事务，并在退出前启动另一个事务
	 * 以匹配PostgresMain()中等待我们的提交。
	 */
	if (fc_use_own_xacts)
	{
		Assert(!fc_in_outer_xact);

		/* ActiveSnapshot未由自动清理设置 */
		if (ActiveSnapshotSet())
			PopActiveSnapshot();

		/* 与PostgresMain()中的StartTransaction匹配 */
		CommitTransactionCommand();
	}

	/* 开启或关闭清理成本会计，并设置/清除in_vacuum */
	PG_TRY();
	{
		ListCell   *fc_cur;

		fc_in_vacuum = true;
		VacuumCostActive = (VacuumCostDelay > 0);
		VacuumCostBalance = 0;
		VacuumPageHit = 0;
		VacuumPageMiss = 0;
		VacuumPageDirty = 0;
		VacuumCostBalanceLocal = 0;
		VacuumSharedCostBalance = NULL;
		VacuumActiveNWorkers = NULL;

		/*
		 * 循环处理每个选定的关系。
		 */
		foreach(fc_cur, fc_relations)
		{
			VacuumRelation *fc_vrel = lfirst_node(VacuumRelation, fc_cur);

			if (fc_params->options & VACOPT_VACUUM)
			{
				if (!fc_vacuum_rel(fc_vrel->oid, fc_vrel->relation, fc_params))
					continue;
			}

			if (fc_params->options & VACOPT_ANALYZE)
			{
				/*
				 * 如果使用单独的事务，则为分析启动一个事务。否则，
				 * 我们可以使用外部事务。
				 */
				if (fc_use_own_xacts)
				{
					StartTransactionCommand();
					/* 索引中的函数可能需要设置快照 */
					PushActiveSnapshot(GetTransactionSnapshot());
				}

				analyze_rel(fc_vrel->oid, fc_vrel->relation, fc_params,
							fc_vrel->va_cols, fc_in_outer_xact, vac_strategy);

				if (fc_use_own_xacts)
				{
					PopActiveSnapshot();
					CommitTransactionCommand();
				}
				else
				{
					/*
					 * 如果我们不使用单独的事务，最好用CCI分隔ANALYZE操作。
					 * 这避免了用户说“ANALYZE t, t”时出现问题。
					 */
					CommandCounterIncrement();
				}
			}
		}
	}
	PG_FINALLY();
	{
		fc_in_vacuum = false;
		VacuumCostActive = false;
	}
	PG_END_TRY();

	/*
	 * 完成处理。
	 */
	if (fc_use_own_xacts)
	{
		/* 在这里，我们不在事务中 */

		/*
		 * 这与在PostgresMain()中等待我们的CommitTransaction相匹配。
		 */
		StartTransactionCommand();
	}

	if ((fc_params->options & VACOPT_VACUUM) && !IsAutoVacuumWorkerProcess())
	{
		/*
		 * 更新pg_database.datfrozenxid，并尽可能截断pg_xact。
		 * （autovacuum.c为其自身执行此操作。）
		 */
		vac_update_datfrozenxid();
	}

	/*
	 * 清理工作存储 --- 请注意，我们必须在 StartTransactionCommand 之后这样做，否则我们可能试图删除活动上下文！
	 */
	MemoryContextDelete(vac_context);
	vac_context = NULL;
}

/*
 * 检查给定关系是否可以安全地进行VACUUM或ANALYZE。
 * 如果用户不是关系的所有者，则发出警告日志消息，并返回false以让调用者决定
 * 如何处理此关系。此例程用于决定关系是否可以进行VACUUM或ANALYZE处理。
 */
bool vacuum_is_relation_owner(Oid fc_relid, Form_pg_class fc_reltuple, bits32 fc_options)
{
	char	   *fc_relname;

	Assert((fc_options & (VACOPT_VACUUM | VACOPT_ANALYZE)) != 0);

	/*
	 * 检查权限。
	 *
	 * 如果用户是超级用户、表的拥有者或数据库的拥有者（但在后者情况下，仅当
	 * 该表不是共享关系时），我们允许用户对表进行清理或分析。pg_class_ownercheck
	 * 包括超级用户的情况。
	 *
	 * 请注意，我们选择将权限失败视为警告，并继续尝试清理或分析数据库的其余部分---
	 * 这样做合适吗？
	 */
	if (pg_class_ownercheck(fc_relid, GetUserId()) ||
		(pg_database_ownercheck(MyDatabaseId, GetUserId()) && !fc_reltuple->relisshared))
		return true;

	fc_relname = NameStr(fc_reltuple->relname);

	if ((fc_options & VACOPT_VACUUM) != 0)
	{
		if (fc_reltuple->relisshared)
			ereport(WARNING,
					(errmsg("skipping \"%s\" --- only superuser can vacuum it",
							fc_relname)));
		else if (fc_reltuple->relnamespace == PG_CATALOG_NAMESPACE)
			ereport(WARNING,
					(errmsg("skipping \"%s\" --- only superuser or database owner can vacuum it",
							fc_relname)));
		else
			ereport(WARNING,
					(errmsg("skipping \"%s\" --- only table or database owner can vacuum it",
							fc_relname)));

		/*
		 * 对于VACUUM ANALYZE，两个日志都有可能出现，但仅生成
		 * VACUUM的信息，因为这是第一个被处理的。
		 */
		return false;
	}

	if ((fc_options & VACOPT_ANALYZE) != 0)
	{
		if (fc_reltuple->relisshared)
			ereport(WARNING,
					(errmsg("skipping \"%s\" --- only superuser can analyze it",
							fc_relname)));
		else if (fc_reltuple->relnamespace == PG_CATALOG_NAMESPACE)
			ereport(WARNING,
					(errmsg("skipping \"%s\" --- only superuser or database owner can analyze it",
							fc_relname)));
		else
			ereport(WARNING,
					(errmsg("skipping \"%s\" --- only table or database owner can analyze it",
							fc_relname)));
	}

	return false;
}


/*
 * vacuum_open_relation
 *
 * 该例程用于尝试打开和锁定将要进行清理或分析的关系。
 * 如果关系无法打开或锁定，尽可能发出日志。
 */
Relation vacuum_open_relation(Oid fc_relid, RangeVar *fc_relation, bits32 fc_options,
					 bool fc_verbose, LOCKMODE fc_lmode)
{
	Relation	fc_rel;
	bool		fc_rel_lock = true;
	int			fc_elevel;

	Assert((fc_options & (VACOPT_VACUUM | VACOPT_ANALYZE)) != 0);

	/*
	 * 打开关系并获取适当的锁。
	 *
	 * 这里有一个竞争条件：关系可能在我们上次看到后消失。
	 * 如果是这样，我们就不需要清理或分析它。
	 *
	 * 如果我们被要求不等待关系锁，首先以非阻塞模式获取它，
	 * 然后再调用try_relation_open()。
	 */
	if (!(fc_options & VACOPT_SKIP_LOCKED))
		fc_rel = try_relation_open(fc_relid, fc_lmode);
	else if (ConditionalLockRelationOid(fc_relid, fc_lmode))
		fc_rel = try_relation_open(fc_relid, NoLock);
	else
	{
		fc_rel = NULL;
		fc_rel_lock = false;
	}

	/* 如果关系已打开，则返回 */
	if (fc_rel)
		return fc_rel;

	/*
	 * 无法打开关系，因此尽可能生成一个日志
	 * 来告知情况。
	 *
	 * 如果RangeVar未定义，我们没有足够的信息来
	 * 提供有意义的日志语句。很可能调用者故意没有提供此信息，
	 * 因此跳过此日志。
	 */
	if (fc_relation == NULL)
		return NULL;

	/*
	 * 确定日志级别。
	 *
	 * 对于手动的VACUUM或ANALYZE，我们发出一个WARNING，以匹配权限检查中的日志
	 * 语句；否则，仅在调用者请求时记录。
	 */
	if (!IsAutoVacuumWorkerProcess())
		fc_elevel = WARNING;
	else if (fc_verbose)
		fc_elevel = LOG;
	else
		return NULL;

	if ((fc_options & VACOPT_VACUUM) != 0)
	{
		if (!fc_rel_lock)
			ereport(fc_elevel,
					(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
					 errmsg("skipping vacuum of \"%s\" --- lock not available",
							fc_relation->relname)));
		else
			ereport(fc_elevel,
					(errcode(ERRCODE_UNDEFINED_TABLE),
					 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
							fc_relation->relname)));

		/*
		 * 对于VACUUM ANALYZE，两个日志都有可能出现，但仅生成
		 * VACUUM的信息，因为这是第一个被处理的。
		 */
		return NULL;
	}

	if ((fc_options & VACOPT_ANALYZE) != 0)
	{
		if (!fc_rel_lock)
			ereport(fc_elevel,
					(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
					 errmsg("skipping analyze of \"%s\" --- lock not available",
							fc_relation->relname)));
		else
			ereport(fc_elevel,
					(errcode(ERRCODE_UNDEFINED_TABLE),
					 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
							fc_relation->relname)));
	}

	return NULL;
}


/*
 * 给定一个VacuumRelation，如果未指定表OID，则填写表OID，
 * 可选地为表的分区添加VacuumRelations。
 *
 * 如果VacuumRelation没有提供OID且是分区表，则会为每个分区
 * 在输出中添加一个额外条目。目前，只有自动清理在调用vacuum()
 * 时提供OID，并且它不希望我们扩展分区表。
 *
 * 我们注意到不修改输入数据结构，而是构建新的
 * VacuumRelation返回。（但是请注意，它们将引用
 * 输入的未修改部分，例如列列表。）新数据结构
 * 在vac_context中创建。
 */
static List * fc_expand_vacuum_rel(VacuumRelation *fc_vrel, int fc_options)
{
	List	   *fc_vacrels = NIL;
	MemoryContext fc_oldcontext;

	/* 如果调用者提供了OID，这里不需要做任何操作。 */
	if (OidIsValid(fc_vrel->oid))
	{
		fc_oldcontext = MemoryContextSwitchTo(vac_context);
		fc_vacrels = lappend(fc_vacrels, fc_vrel);
		MemoryContextSwitchTo(fc_oldcontext);
	}
	else
	{
		/* 处理特定关系，可能还处理其分区 */
		Oid			fc_relid;
		HeapTuple	fc_tuple;
		Form_pg_class fc_classForm;
		bool		fc_include_parts;
		int			fc_rvr_opts;

		/*
		 * 由于自动清理工作线程在调用vacuum()时提供OID，因此
		 * 任何自动清理工作线程都不应达到此代码。
		 */
		Assert(!IsAutoVacuumWorkerProcess());

		/*
		 * 我们暂时获取 AccessShareLock 以保护下面的 syscache 查找，
		 * 以及 find_all_inheritors 对调用者持有某个锁的期望。
		 */
		fc_rvr_opts = (fc_options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
		fc_relid = RangeVarGetRelidExtended(fc_vrel->relation,
										 AccessShareLock,
										 fc_rvr_opts,
										 NULL, NULL);

		/*
		 * 如果锁不可用，发出与 vacuum_rel() 和 analyze_rel() 相同的日志语句。
		 */
		if (!OidIsValid(fc_relid))
		{
			if (fc_options & VACOPT_VACUUM)
				ereport(WARNING,
						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
						 errmsg("skipping vacuum of \"%s\" --- lock not available",
								fc_vrel->relation->relname)));
			else
				ereport(WARNING,
						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
						 errmsg("skipping analyze of \"%s\" --- lock not available",
								fc_vrel->relation->relname)));
			return fc_vacrels;
		}

		/*
		 * 要检查关系是否为分区表及其所有权，请获取其 syscache 条目。
		 */
		fc_tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(fc_relid));
		if (!HeapTupleIsValid(fc_tuple))
			elog(ERROR, "cache lookup failed for relation %u", fc_relid);
		fc_classForm = (Form_pg_class) GETSTRUCT(fc_tuple);

		/*
		 * 如果用户是适当的所有者，为此关系创建一个可返回的 VacuumRelation。
		 */
		if (vacuum_is_relation_owner(fc_relid, fc_classForm, fc_options))
		{
			fc_oldcontext = MemoryContextSwitchTo(vac_context);
			fc_vacrels = lappend(fc_vacrels, makeVacuumRelation(fc_vrel->relation,
														  fc_relid,
														  fc_vrel->va_cols));
			MemoryContextSwitchTo(fc_oldcontext);
		}


		fc_include_parts = (fc_classForm->relkind == RELKIND_PARTITIONED_TABLE);
		ReleaseSysCache(fc_tuple);

		/*
		 * 如果是，就为其分区创建关系列表条目。请注意,
		 * find_all_inheritors() 返回的列表包括传入的 OID,
		 * 所以我们必须跳过它。暂时没有必要对个别分区加锁，
		 * 这样做只会增加不必要的死锁风险。出于这个原因，
		 * 我们暂时不检查分区的所有权，这些分区会被添加到待处理列表中。
		 * 所有权将会在之后检查。
		 */
		if (fc_include_parts)
		{
			List	   *fc_part_oids = find_all_inheritors(fc_relid, NoLock, NULL);
			ListCell   *fc_part_lc;

			foreach(fc_part_lc, fc_part_oids)
			{
				Oid			fc_part_oid = lfirst_oid(fc_part_lc);

				if (fc_part_oid == fc_relid)
					continue;	/* 忽略原始表 */

				/*
				 * 我们省略一个 RangeVar，因为在后面关于无法打开这些关系的抱怨 
				 * 似乎不合适。
				 */
				fc_oldcontext = MemoryContextSwitchTo(vac_context);
				fc_vacrels = lappend(fc_vacrels, makeVacuumRelation(NULL,
															  fc_part_oid,
															  fc_vrel->va_cols));
				MemoryContextSwitchTo(fc_oldcontext);
			}
		}

		/*
		 * 再次释放锁。这意味着在我们实际尝试处理表时，它可能已经消失或更名。
		 * 在前一种情况下，我们会默默忽略它；在后一种情况下我们仍会处理它，
		 * 但必须注意 RangeVar 可能不再能够明确识别它。这或许并不理想，
		 * 但几乎没有实用的替代方案，因为我们通常会在这期间提交该事务并开始一个新事务。
		 * 此外，保持多个关系的锁会带来显著的死锁风险。
		 */
		UnlockRelationOid(fc_relid, AccessShareLock);
	}

	return fc_vacrels;
}

/*
 * 构建当前数据库中所有可清理关系的 VacuumRelations 列表。
 * 该列表在 vac_context 中构建。
 */
static List * fc_get_all_vacuum_rels(int fc_options)
{
	List	   *fc_vacrels = NIL;
	Relation	fc_pgclass;
	TableScanDesc fc_scan;
	HeapTuple	fc_tuple;

	fc_pgclass = table_open(RelationRelationId, AccessShareLock);

	fc_scan = table_beginscan_catalog(fc_pgclass, 0, NULL);

	while ((fc_tuple = heap_getnext(fc_scan, ForwardScanDirection)) != NULL)
	{
		Form_pg_class fc_classForm = (Form_pg_class) GETSTRUCT(fc_tuple);
		MemoryContext fc_oldcontext;
		Oid			fc_relid = fc_classForm->oid;

		/* 检查关系的权限 */
		if (!vacuum_is_relation_owner(fc_relid, fc_classForm, fc_options))
			continue;

		/*
		 * 我们在这里包括分区表；具体进行哪种操作由调用者决定是否处理或忽略它们。
		 */
		if (fc_classForm->relkind != RELKIND_RELATION &&
			fc_classForm->relkind != RELKIND_MATVIEW &&
			fc_classForm->relkind != RELKIND_PARTITIONED_TABLE)
			continue;

		/*
		 * 构建指定要处理的表 OID 的 VacuumRelation(s)。
		 * 我们省略一个 RangeVar，因为在后面关于无法打开这些关系的抱怨
		 * 似乎不合适。
		 */
		fc_oldcontext = MemoryContextSwitchTo(vac_context);
		fc_vacrels = lappend(fc_vacrels, makeVacuumRelation(NULL,
													  fc_relid,
													  NIL));
		MemoryContextSwitchTo(fc_oldcontext);
	}

	table_endscan(fc_scan);
	table_close(fc_pgclass, AccessShareLock);

	return fc_vacrels;
}

/*
 * vacuum_set_xid_limits() -- 计算 oldestXmin 和 freeze 截止点
 *
 * 输入参数是目标关系及适用的 freeze age 设置。
 *
 * 输出参数是：
 * - oldestXmin 是 Xid，低于此 Xid 的元组被任何已提交的事务删除，应被视为 DEAD，而不仅仅是 RECENTLY_DEAD。
 * - oldestMxact 是 Mxid，低于此 Mxid 的 MultiXacts 在任何运行的事务中肯定不可见。
 * - freezeLimit 是 Xid，低于此 Xid 的所有 Xids 在激进的清理过程中肯定会被 FrozenTransactionId 替换。
 * - multiXactCutoff 是值，低于此值的所有 MultiXactIds 在激进的清理过程中肯定会从 Xmax 中移除。
 *
 * 返回值指示 vacuumlazy.c 调用者是否应该进行激进的 VACUUM 操作。激进的 VACUUM 必须将 relfrozenxid 至少推进到 FreezeLimit，并将 relminmxid 至少推进到 multiXactCutoff。
 *
 * oldestXmin 和 oldestMxact 是可以被我们的 vacuumlazy.c 调用者在稍后传递给 vac_update_relstats() 作为 frozenxid 和 minmulti 参数的最新值。当 VACUUM 将不会在表中留下任何未冻结的 XIDs/XMIDs 时，这些值应该被传递。
 */
bool vacuum_set_xid_limits(Relation fc_rel,
					  int fc_freeze_min_age,
					  int fc_freeze_table_age,
					  int fc_multixact_freeze_min_age,
					  int fc_multixact_freeze_table_age,
					  TransactionId *fc_oldestXmin,
					  MultiXactId *fc_oldestMxact,
					  TransactionId *fc_freezeLimit,
					  MultiXactId *fc_multiXactCutoff)
{
	int			fc_freezemin;
	int			fc_mxid_freezemin;
	int			fc_effective_multixact_freeze_max_age;
	TransactionId fc_limit;
	TransactionId fc_safeLimit;
	MultiXactId fc_mxactLimit;
	MultiXactId fc_safeMxactLimit;
	int			fc_freezetable;

	/*
	 * 我们总是可以忽略正在运行的懒惰清理进程。这是因为我们只使用这些值来决定我们必须在表中保留哪些元组。由于懒惰清理不会在任何地方写入它的 XID（通常没有分配 XID），因此忽略它是安全的。从理论上讲，在完全清理中忽略懒惰清理可能会有问题，但请记住，在任何时候只有一个清理进程可以在特定表上工作，而且每个清理总是一个独立的事务。
	 */
	*fc_oldestXmin = GetOldestNonRemovableTransactionId(fc_rel);

	if (OldSnapshotThresholdActive())
	{
		TransactionId fc_limit_xmin;
		TimestampTz fc_limit_ts;

		if (TransactionIdLimitedForOldSnapshots(*fc_oldestXmin, fc_rel,
												&fc_limit_xmin, &fc_limit_ts))
		{
			/*
			 * TODO: 只有在根据增加的限制进行修剪时，我们才应该设置阈值。在这里并不如机遇修剪（通常发生在更高频率的情况下）那么关键，但仍然会是一个显著的改进。
			 */
			SetOldSnapshotThresholdTimestamp(fc_limit_ts, fc_limit_xmin);
			*fc_oldestXmin = fc_limit_xmin;
		}
	}

	Assert(TransactionIdIsNormal(*fc_oldestXmin));

	/*
	 * 确定使用的最小冻结年龄：如调用者所指定的，或 vacuum_freeze_min_age，但在任何情况下不超过 autovacuum_freeze_max_age 的一半，以便防止 XID 旋转的 autovacuum 不会太频繁发生。
	 */
	fc_freezemin = fc_freeze_min_age;
	if (fc_freezemin < 0)
		fc_freezemin = vacuum_freeze_min_age;
	fc_freezemin = Min(fc_freezemin, autovacuum_freeze_max_age / 2);
	Assert(fc_freezemin >= 0);

	/*
	 * 计算截止 XID，注意不要生成 “永久” XID
	 */
	fc_limit = *fc_oldestXmin - fc_freezemin;
	if (!TransactionIdIsNormal(fc_limit))
		fc_limit = FirstNormalTransactionId;

	/*
	 * 如果 oldestXmin 非常久远（实际上，超过 autovacuum_freeze_max_age / 2 的 XIDs），则抱怨并强制设置最小冻结年龄为零。
	 */
	fc_safeLimit = ReadNextTransactionId() - autovacuum_freeze_max_age;
	if (!TransactionIdIsNormal(fc_safeLimit))
		fc_safeLimit = FirstNormalTransactionId;

	if (TransactionIdPrecedes(fc_limit, fc_safeLimit))
	{
		ereport(WARNING,
				(errmsg("oldest xmin is far in the past"),
				 errhint("Close open transactions soon to avoid wraparound problems.\n"
						 "You might also need to commit or roll back old prepared transactions, or drop stale replication slots.")));
		fc_limit = *fc_oldestXmin;
	}

	*fc_freezeLimit = fc_limit;

	/*
	 * 计算冻结紧急的 multixact 年龄。这通常是 autovacuum_multixact_freeze_max_age，但如果我们缺少 multixact 成员空间，则可能会更少。
	 */
	fc_effective_multixact_freeze_max_age = MultiXactMemberFreezeThreshold();

	/*
	 * 确定要使用的最小 multixact 冻结年龄：根据调用者的指定，或 vacuum_multixact_freeze_min_age，但无论如何不超过 effective_multixact_freeze_max_age 的一半，以便自动清理以防止 MultiXact 回绕不会过于频繁发生。
	 */
	fc_mxid_freezemin = fc_multixact_freeze_min_age;
	if (fc_mxid_freezemin < 0)
		fc_mxid_freezemin = vacuum_multixact_freeze_min_age;
	fc_mxid_freezemin = Min(fc_mxid_freezemin,
						 fc_effective_multixact_freeze_max_age / 2);
	Assert(fc_mxid_freezemin >= 0);

	/* 为调用者保留 */
	*fc_oldestMxact = GetOldestMultiXactId();

	/* 计算截止 multi，注意生成有效值 */
	fc_mxactLimit = *fc_oldestMxact - fc_mxid_freezemin;
	if (fc_mxactLimit < FirstMultiXactId)
		fc_mxactLimit = FirstMultiXactId;

	fc_safeMxactLimit =
		ReadNextMultiXactId() - fc_effective_multixact_freeze_max_age;
	if (fc_safeMxactLimit < FirstMultiXactId)
		fc_safeMxactLimit = FirstMultiXactId;

	if (MultiXactIdPrecedes(fc_mxactLimit, fc_safeMxactLimit))
	{
		ereport(WARNING,
				(errmsg("oldest multixact is far in the past"),
				 errhint("Close open transactions with multixacts soon to avoid wraparound problems.")));
		/* 使用安全限制，除非较旧的 mxact 仍在运行 */
		if (MultiXactIdPrecedes(*fc_oldestMxact, fc_safeMxactLimit))
			fc_mxactLimit = *fc_oldestMxact;
		else
			fc_mxactLimit = fc_safeMxactLimit;
	}

	*fc_multiXactCutoff = fc_mxactLimit;

	/*
	 * 完成输出参数的设置；只需弄清楚调用者是否需要进行激进的 VACUUM。
	 *
	 * 确定要使用的表冻结年龄：根据调用者的指定，或 vacuum_freeze_table_age，但无论如何不能超过 autovacuum_freeze_max_age * 0.95，以便如果您有例如每晚的 VACUUM 计划，每晚的 VACUUM 有机会在启动反回绕自动清理之前冻结元组。
	 */
	fc_freezetable = fc_freeze_table_age;
	if (fc_freezetable < 0)
		fc_freezetable = vacuum_freeze_table_age;
	fc_freezetable = Min(fc_freezetable, autovacuum_freeze_max_age * 0.95);
	Assert(fc_freezetable >= 0);

	/*
	 * 计算导致激进清理的 XID 限制，注意不要生成 "永久" XID
	 */
	fc_limit = ReadNextTransactionId() - fc_freezetable;
	if (!TransactionIdIsNormal(fc_limit))
		fc_limit = FirstNormalTransactionId;
	if (TransactionIdPrecedesOrEquals(fc_rel->rd_rel->relfrozenxid,
									  fc_limit))
		return true;

	/*
	 * 与上述类似，确定用于 multixacts 的表冻结年龄：根据调用者的指定，或 vacuum_multixact_freeze_table_age，但无论如何不超过 autovacuum_multixact_freeze_table_age * 0.95，以便如果您有例如每晚的 VACUUM 计划，每晚的 VACUUM 有机会在启动反回绕自动清理之前冻结 multixacts。
	 */
	fc_freezetable = fc_multixact_freeze_table_age;
	if (fc_freezetable < 0)
		fc_freezetable = vacuum_multixact_freeze_table_age;
	fc_freezetable = Min(fc_freezetable,
					  fc_effective_multixact_freeze_max_age * 0.95);
	Assert(fc_freezetable >= 0);

	/*
	 * 计算导致激进清理的 MultiXact 限制，注意生成有效的 MultiXact 值
	 */
	fc_mxactLimit = ReadNextMultiXactId() - fc_freezetable;
	if (fc_mxactLimit < FirstMultiXactId)
		fc_mxactLimit = FirstMultiXactId;
	if (MultiXactIdPrecedesOrEquals(fc_rel->rd_rel->relminmxid,
									fc_mxactLimit))
		return true;

	return false;
}

/*
 * vacuum_xid_failsafe_check() -- 被 VACUUM 的回绕安全机制使用，以确定其表的 relfrozenxid 和 relminmxid 现在是否危险地远在过去。
 *
 * 输入参数是目标关系的 relfrozenxid 和 relminmxid。
 *
 * 当我们返回 true 时，VACUUM 调用者触发安全机制。
 */
bool vacuum_xid_failsafe_check(TransactionId fc_relfrozenxid, MultiXactId fc_relminmxid)
{
	TransactionId fc_xid_skip_limit;
	MultiXactId fc_multi_skip_limit;
	int			fc_skip_index_vacuum;

	Assert(TransactionIdIsNormal(fc_relfrozenxid));
	Assert(MultiXactIdIsValid(fc_relminmxid));

	/*
	 * 确定要使用的索引跳过年龄。无论如何不低于 autovacuum_freeze_max_age * 1.05。
	 */
	fc_skip_index_vacuum = Max(vacuum_failsafe_age, autovacuum_freeze_max_age * 1.05);

	fc_xid_skip_limit = ReadNextTransactionId() - fc_skip_index_vacuum;
	if (!TransactionIdIsNormal(fc_xid_skip_limit))
		fc_xid_skip_limit = FirstNormalTransactionId;

	if (TransactionIdPrecedes(fc_relfrozenxid, fc_xid_skip_limit))
	{
		/* 表的 relfrozenxid 太旧 */
		return true;
	}

	/*
	 * 与上述类似，确定用于 multixact 的索引跳过年龄。无论如何不低于 autovacuum_multixact_freeze_max_age * 1.05。
	 */
	fc_skip_index_vacuum = Max(vacuum_multixact_failsafe_age,
							autovacuum_multixact_freeze_max_age * 1.05);

	fc_multi_skip_limit = ReadNextMultiXactId() - fc_skip_index_vacuum;
	if (fc_multi_skip_limit < FirstMultiXactId)
		fc_multi_skip_limit = FirstMultiXactId;

	if (MultiXactIdPrecedes(fc_relminmxid, fc_multi_skip_limit))
	{
		/* 表的 relminmxid 太旧 */
		return true;
	}

	return false;
}

/*
 * vac_estimate_reltuples() -- 估计 pg_class.reltuples 的新值
 *
 *		如果我们扫描了整个关系，那么我们应该直接使用看到的活元组的计数；但如果我们没有这样做，我们不应盲目地从该数字推断，因为 VACUUM 可能已经扫描了一个相当非随机的表的子集。当我们只有部分信息时，我们将 pg_class.reltuples/pg_class.relpages 的旧值作为未扫描页面中元组密度的测量。
 *
 *		注意：scanned_tuples 应仅统计 *活* 元组，因为 pg_class.reltuples 是这样定义的。
 */
double vac_estimate_reltuples(Relation fc_relation,
					   BlockNumber fc_total_pages,
					   BlockNumber fc_scanned_pages,
					   double fc_scanned_tuples)
{
	BlockNumber fc_old_rel_pages = fc_relation->rd_rel->relpages;
	double		fc_old_rel_tuples = fc_relation->rd_rel->reltuples;
	double		fc_old_density;
	double		fc_unscanned_pages;
	double		fc_total_tuples;

	/* 如果我们确实扫描了整个表，直接使用计数作为最终值 */
	if (fc_scanned_pages >= fc_total_pages)
		return fc_scanned_tuples;

	/*
	 * 当连续的 VACUUM 命令一次又一次扫描相同的几页时，
	 * 而表中的内容实际上没有任何变化，存在我们对元组密度的信念
	 * 逐渐扭曲的风险。这可能是由于 vacuumlazy.c 的实现细节所致，
	 * 例如它总是倾向于扫描最后一个堆页面。在这里处理。
	 *
	 * 如果关系在现有的 pg_class 条目中大小是 _完全_ 一样的，
	 * 并且只有少数几页（少于 2%）被扫描，保持 reltuples 的现有值。
	 * 当仅扫描 rel 的页面的一个子集 <= 单个页面 时，也保持现有值。
	 *
	 * （注意：我们可能在这里返回 -1。）
	 */
	if (fc_old_rel_pages == fc_total_pages &&
		fc_scanned_pages < (double) fc_total_pages * 0.02)
		return fc_old_rel_tuples;
	if (fc_scanned_pages <= 1)
		return fc_old_rel_tuples;

	/*
	 * 如果旧的密度未知，我们只能将
	 * scanned_tuples 按比例放大以匹配 total_pages。
	 */
	if (fc_old_rel_tuples < 0 || fc_old_rel_pages == 0)
		return floor((fc_scanned_tuples / fc_scanned_pages) * fc_total_pages + 0.5);

	/*
	 * 好的，我们已经涵盖了边缘案例。正常计算是将
	 * 旧的测量值转换为密度（每页元组），然后
	 * 使用该数值估计未扫描页面中的元组数量，
	 * 最后添加扫描页面中的元组数量。
	 */
	fc_old_density = fc_old_rel_tuples / fc_old_rel_pages;
	fc_unscanned_pages = (double) fc_total_pages - (double) fc_scanned_pages;
	fc_total_tuples = fc_old_density * fc_unscanned_pages + fc_scanned_tuples;
	return floor(fc_total_tuples + 0.5);
}


/*
 *	vac_update_relstats() -- 更新一个关系的统计信息
 *
 *		更新保存在其 pg_class 行中的整个关系统计信息。如果我们正在
 *		进行分析，还会更新其他统计信息，但我们总是更新这些统计信息。
 *		此例程适用于 pg_class 中的索引和堆关系条目。
 *
 *		我们在这里违反了事务语义，通过用新值覆盖关系的现有 pg_class 元组。
 *		这在我们确定新值是否正确时是相对安全的，无论此事务是否提交。
 *		这样做的原因是，如果我们以通常的方式更新这些元组，清理 pg_class
 *		本身将不会很好地工作 —— 当我们完成清理循环时，pg_class 中的大多数
 *		元组都将变得过时。当然，这仅适用于固定大小的非空列，但这些就是。
 *
 *		这样做的另一个原因是，当我们处于惰性清理并且设置了 PROC_IN_VACUUM
 *		时，我们不能执行任何常规更新。正在清理 pg_class 的人可能会认为
 *		他们可以删除标记为 xmin = 我们的 xid 的元组。
 *
 *		除了基本上是非事务性的统计信息，例如 relpages 和 relallvisible，
 *		我们还尝试通过在不再正确时清除它们，来维护某些惰性更新的 DDL 标志，
 *		例如 relhasindex。在 VACUUM 中这样做是安全的，因为 VACUUM
 *		不能与 CREATE INDEX/RULE/TRIGGER 并行运行，并且不能是事务
 *		块的一部分。然而，在外部事务中的 ANALYZE 中这样做则*不*安全，
 *		例如当前事务可能已经删除了最后一个索引；然后我们会认为应该清除
 *		relhasindex，但如果事务稍后回滚，则这将是错误的。因此，如果我们
 *		在外部事务中，则不更新 DDL 标志。这是可以的，因为延迟标志维护是
 *		始终允许的。
 *
 *		注意：num_tuples 应仅计算 *活* 元组，因为 pg_class.reltuples
 *		就是这样定义的。
 *
 *		此例程共享给 VACUUM 和 ANALYZE。
 */
void vac_update_relstats(Relation fc_relation,
					BlockNumber fc_num_pages, double fc_num_tuples,
					BlockNumber fc_num_all_visible_pages,
					bool fc_hasindex, TransactionId fc_frozenxid,
					MultiXactId fc_minmulti,
					bool *fc_frozenxid_updated, bool *fc_minmulti_updated,
					bool fc_in_outer_xact)
{
	Oid			fc_relid = RelationGetRelid(fc_relation);
	Relation	fc_rd;
	ScanKeyData fc_key[1];
	HeapTuple	fc_ctup;
	void	   *fc_inplace_state;
	Form_pg_class fc_pgcform;
	bool		fc_dirty,
				fc_futurexid,
				fc_futuremxid;
	TransactionId fc_oldfrozenxid;
	MultiXactId fc_oldminmulti;

	fc_rd = table_open(RelationRelationId, RowExclusiveLock);

	/* 获取元组的副本以进行修改 */
	ScanKeyInit(&fc_key[0],
				Anum_pg_class_oid,
				BTEqualStrategyNumber, F_OIDEQ,
				ObjectIdGetDatum(fc_relid));
	systable_inplace_update_begin(fc_rd, ClassOidIndexId, true,
								  NULL, 1, fc_key, &fc_ctup, &fc_inplace_state);
	if (!HeapTupleIsValid(fc_ctup))
		elog(ERROR, "pg_class entry for relid %u vanished during vacuuming",
			 fc_relid);
	fc_pgcform = (Form_pg_class) GETSTRUCT(fc_ctup);

	/* 对复制的元组应用统计更新（如果有） */

	fc_dirty = false;
	if (fc_pgcform->relpages != (int32) fc_num_pages)
	{
		fc_pgcform->relpages = (int32) fc_num_pages;
		fc_dirty = true;
	}
	if (fc_pgcform->reltuples != (float4) fc_num_tuples)
	{
		fc_pgcform->reltuples = (float4) fc_num_tuples;
		fc_dirty = true;
	}
	if (fc_pgcform->relallvisible != (int32) fc_num_all_visible_pages)
	{
		fc_pgcform->relallvisible = (int32) fc_num_all_visible_pages;
		fc_dirty = true;
	}

	/* 应用 DDL 更新，但不能在外部事务中（见上文） */

	if (!fc_in_outer_xact)
	{
		/*
		 * 如果我们没有找到任何索引，重置 relhasindex。
		 */
		if (fc_pgcform->relhasindex && !fc_hasindex)
		{
			fc_pgcform->relhasindex = false;
			fc_dirty = true;
		}

		/* 如果需要，我们还清除 relhasrules 和 relhastriggers */
		if (fc_pgcform->relhasrules && fc_relation->rd_rules == NULL)
		{
			fc_pgcform->relhasrules = false;
			fc_dirty = true;
		}
		if (fc_pgcform->relhastriggers && fc_relation->trigdesc == NULL)
		{
			fc_pgcform->relhastriggers = false;
			fc_dirty = true;
		}
	}

	/*
	 * 更新 relfrozenxid，除非调用者传递 InvalidTransactionId
	 * 表示它没有新数据。
	 *
	 * 通常情况下，我们不允许 relfrozenxid 回退。然而，如果存储的
	 * relfrozenxid "在未来"，则最好假设它已损坏，并用表中最旧的剩余 XID
	 * 覆盖。这应该与 vac_update_datfrozenxid() 匹配，关于我们认为
	 * "在未来" 的定义。
	 */
	fc_oldfrozenxid = fc_pgcform->relfrozenxid;
	fc_futurexid = false;
	if (fc_frozenxid_updated)
		*fc_frozenxid_updated = false;
	if (TransactionIdIsNormal(fc_frozenxid) && fc_oldfrozenxid != fc_frozenxid)
	{
		bool		fc_update = false;

		if (TransactionIdPrecedes(fc_oldfrozenxid, fc_frozenxid))
			fc_update = true;
		else if (TransactionIdPrecedes(ReadNextTransactionId(), fc_oldfrozenxid))
			fc_futurexid = fc_update = true;

		if (fc_update)
		{
			fc_pgcform->relfrozenxid = fc_frozenxid;
			fc_dirty = true;
			if (fc_frozenxid_updated)
				*fc_frozenxid_updated = true;
		}
	}

	/* 对于 relminmxid 也类似 */
	fc_oldminmulti = fc_pgcform->relminmxid;
	fc_futuremxid = false;
	if (fc_minmulti_updated)
		*fc_minmulti_updated = false;
	if (MultiXactIdIsValid(fc_minmulti) && fc_oldminmulti != fc_minmulti)
	{
		bool		fc_update = false;

		if (MultiXactIdPrecedes(fc_oldminmulti, fc_minmulti))
			fc_update = true;
		else if (MultiXactIdPrecedes(ReadNextMultiXactId(), fc_oldminmulti))
			fc_futuremxid = fc_update = true;

		if (fc_update)
		{
			fc_pgcform->relminmxid = fc_minmulti;
			fc_dirty = true;
			if (fc_minmulti_updated)
				*fc_minmulti_updated = true;
		}
	}

	/* 如果有任何更改，写出元组。 */
	if (fc_dirty)
		systable_inplace_update_finish(fc_inplace_state, fc_ctup);
	else
		systable_inplace_update_cancel(fc_inplace_state);

	table_close(fc_rd, RowExclusiveLock);

	if (fc_futurexid)
		ereport(WARNING,
				(errcode(ERRCODE_DATA_CORRUPTED),
				 errmsg_internal("overwrote invalid relfrozenxid value %u with new value %u for table \"%s\"",
								 fc_oldfrozenxid, fc_frozenxid,
								 RelationGetRelationName(fc_relation))));
	if (fc_futuremxid)
		ereport(WARNING,
				(errcode(ERRCODE_DATA_CORRUPTED),
				 errmsg_internal("overwrote invalid relminmxid value %u with new value %u for table \"%s\"",
								 fc_oldminmulti, fc_minmulti,
								 RelationGetRelationName(fc_relation))));
}


/*
 *	vac_update_datfrozenxid() -- 更新我们的数据库的 pg_database.datfrozenxid
 *
 *		将 pg_database 的 datfrozenxid 条目更新为最低的 pg_class.relfrozenxid 值。
 *
 *		同样，将我们的 datminmxid 更新为最低的 pg_class.relminmxid 值。
 *
 *		如果我们能够推进任何 pg_database 值，也尝试截断 pg_xact 和 pg_multixact。
 *
 *		我们在这里违反了事务语义，通过用新值覆盖数据库现有的 pg_database 元组。 这是相对安全的，因为无论该事务是否提交，新值都是正确的。 与 vac_update_relstats 一样，这避免了在 VACUUM 后留下死元组。
 */
void vac_update_datfrozenxid(void)
{
	HeapTuple	fc_tuple;
	Form_pg_database fc_dbform;
	Relation	fc_relation;
	SysScanDesc fc_scan;
	HeapTuple	fc_classTup;
	TransactionId fc_newFrozenXid;
	MultiXactId fc_newMinMulti;
	TransactionId fc_lastSaneFrozenXid;
	MultiXactId fc_lastSaneMinMulti;
	bool		fc_bogus = false;
	bool		fc_dirty = false;
	ScanKeyData fc_key[1];
	void	   *fc_inplace_state;

	/*
	 * 将此任务限制为每个数据库一个后端。 这避免了会使 datfrozenxid 或 datminmxid 值向后移动的竞争条件。 这可以避免在 datfrozenxid 之前调用 vac_truncate_clog()，以及对早期 vac_truncate_clog() 调用传递的 datfrozenxid。
	 */
	LockDatabaseFrozenIds(ExclusiveLock);

	/*
	 * 使用 GetOldestNonRemovableTransactionId() 初始化 "min" 计算，这是对于尚未提交的 pg_class 新表条目的 relfrozenxid 的合理近似值；请参见 AddNewRelationTuple()。 所以我们不能通过从此开始产生错误的最小值。
	 */
	fc_newFrozenXid = GetOldestNonRemovableTransactionId(NULL);

	/*
	 * 同样，用将在 pg_class 中用于新表的值初始化 MultiXact "min"。 请参见 AddNewRelationTuple()。
	 */
	fc_newMinMulti = GetOldestMultiXactId();

	/*
	 * 确定我们在扫描期间可以有效看到的最新 relfrozenxid 和 relminmxid 值。 这些是保守值，但实际上不值得尝试更精确。
	 */
	fc_lastSaneFrozenXid = ReadNextTransactionId();
	fc_lastSaneMinMulti = ReadNextMultiXactId();

	/*
	 * 我们必须顺序扫描 pg_class 以找到最小的 Xid，因为没有可以帮助我们的索引。
	 *
	 * 请参见 vac_truncate_clog() 以防止竞争条件。
	 */
	fc_relation = table_open(RelationRelationId, AccessShareLock);

	fc_scan = systable_beginscan(fc_relation, InvalidOid, false,
							  NULL, 0, NULL);

	while ((fc_classTup = systable_getnext(fc_scan)) != NULL)
	{
		volatile FormData_pg_class *fc_classForm = (Form_pg_class) GETSTRUCT(fc_classTup);
		TransactionId fc_relfrozenxid = fc_classForm->relfrozenxid;
		TransactionId fc_relminmxid = fc_classForm->relminmxid;

		/*
		 * 仅考虑能够保存未冻结 XID 的关系（其他任何内容在 relfrozenxid 中应该无效）。
		 */
		if (fc_classForm->relkind != RELKIND_RELATION &&
			fc_classForm->relkind != RELKIND_MATVIEW &&
			fc_classForm->relkind != RELKIND_TOASTVALUE)
		{
			Assert(!TransactionIdIsValid(fc_relfrozenxid));
			Assert(!MultiXactIdIsValid(fc_relminmxid));
			continue;
		}

		/*
		 * 一些表 AM 可能不需要每个关系的 xid / multixid 地平线。 因此，允许 relfrozenxid 和 relminmxid 不被设置（即，设置为各自的 Invalid*Id）似乎是合理的。 因此，只有在设置时才验证和计算每个的地平线。
		 *
		 * 如果一切正常，则没有关系的 relfrozenxid 或 relminmxid 应该是 “未来中的”。 然而，由于 pg_upgrade 中的错误，确实已知会出现这种情况。 如果我们看到任何“未来中的”条目，请放弃并不做任何事情。 这确保我们不会在这些关系被扫描和清理之前截断 clog 和 multixact SLRU。
		 */

		if (TransactionIdIsValid(fc_relfrozenxid))
		{
			Assert(TransactionIdIsNormal(fc_relfrozenxid));

			/* 检查未来的值 */
			if (TransactionIdPrecedes(fc_lastSaneFrozenXid, fc_relfrozenxid))
			{
				fc_bogus = true;
				break;
			}

			/* 确定新的地平线 */
			if (TransactionIdPrecedes(fc_relfrozenxid, fc_newFrozenXid))
				fc_newFrozenXid = fc_relfrozenxid;
		}

		if (MultiXactIdIsValid(fc_relminmxid))
		{
			/* 检查未来的值 */
			if (MultiXactIdPrecedes(fc_lastSaneMinMulti, fc_relminmxid))
			{
				fc_bogus = true;
				break;
			}

			/* 确定新的地平线 */
			if (MultiXactIdPrecedes(fc_relminmxid, fc_newMinMulti))
				fc_newMinMulti = fc_relminmxid;
		}
	}

	/* 我们完成了 pg_class */
	systable_endscan(fc_scan);
	table_close(fc_relation, AccessShareLock);

	/* 如果发现虚假数据则放弃 */
	if (fc_bogus)
		return;

	Assert(TransactionIdIsNormal(fc_newFrozenXid));
	Assert(MultiXactIdIsValid(fc_newMinMulti));

	/* 现在获取我们需要更新的 pg_database 元组。 */
	fc_relation = table_open(DatabaseRelationId, RowExclusiveLock);

	/*
	 * 获取一个副本以进行修改。我们可以先检查 syscache
	 * 元组。如果那得出的结论是 !dirty，我们将避免等待
	 * 并发的 heap_update()，并且会避免独占锁定缓冲区。
	 * 目前，不要优化这部分。
	 */
	ScanKeyInit(&fc_key[0],
				Anum_pg_database_oid,
				BTEqualStrategyNumber, F_OIDEQ,
				ObjectIdGetDatum(MyDatabaseId));

	systable_inplace_update_begin(fc_relation, DatabaseOidIndexId, true,
								  NULL, 1, fc_key, &fc_tuple, &fc_inplace_state);

	if (!HeapTupleIsValid(fc_tuple))
		elog(ERROR, "could not find tuple for database %u", MyDatabaseId);

	fc_dbform = (Form_pg_database) GETSTRUCT(fc_tuple);

	/*
	 * 如同 vac_update_relstats()，我们通常不想让
	 * datfrozenxid 向后回退；但是如果它在“未来”，那么它一定是
	 * 损坏的，覆盖它似乎是最佳选择。
	 */
	if (fc_dbform->datfrozenxid != fc_newFrozenXid &&
		(TransactionIdPrecedes(fc_dbform->datfrozenxid, fc_newFrozenXid) ||
		 TransactionIdPrecedes(fc_lastSaneFrozenXid, fc_dbform->datfrozenxid)))
	{
		fc_dbform->datfrozenxid = fc_newFrozenXid;
		fc_dirty = true;
	}
	else
		fc_newFrozenXid = fc_dbform->datfrozenxid;

	/* 对于 datminmxid 也是如此 */
	if (fc_dbform->datminmxid != fc_newMinMulti &&
		(MultiXactIdPrecedes(fc_dbform->datminmxid, fc_newMinMulti) ||
		 MultiXactIdPrecedes(fc_lastSaneMinMulti, fc_dbform->datminmxid)))
	{
		fc_dbform->datminmxid = fc_newMinMulti;
		fc_dirty = true;
	}
	else
		fc_newMinMulti = fc_dbform->datminmxid;

	if (fc_dirty)
		systable_inplace_update_finish(fc_inplace_state, fc_tuple);
	else
		systable_inplace_update_cancel(fc_inplace_state);

	heap_freetuple(fc_tuple);
	table_close(fc_relation, RowExclusiveLock);

	/*
	 * 如果我们能够推进 datfrozenxid 或 datminmxid，看看我们是否可以
	 * 截断 pg_xact 和/或 pg_multixact。如果共享
	 * XID-wrap-limit 信息过时，也要执行此操作，因为该操作也将
	 * 更新该信息。
	 */
	if (fc_dirty || ForceTransactionIdLimitUpdate())
		fc_vac_truncate_clog(fc_newFrozenXid, fc_newMinMulti,
						  fc_lastSaneFrozenXid, fc_lastSaneMinMulti);
}


/*
 *	vac_truncate_clog() -- 尝试截断提交日志
 *
 *		扫描 pg_database 以确定系统范围内最旧的 datfrozenxid，
 *		并使用它来截断事务提交日志 (pg_xact)。
 *		同时更新 varsup.c 中维护的 XID wrap limit 信息。
 *		datminmxid 也是如此。
 *
 *		传递的 frozenXID 和 minMulti 是我自己
 *		pg_database 实体的更新值。它们用于初始化“最小”
 *		计算。调用者还传递“最后一个有效的”XID 和 MXID，因为它已经
 *		掌握了这些信息。
 *
 *		当我们成功更改了数据库的 datfrozenxid/datminmxid 值，或者我们发现共享
 *		XID-wrap-limit 信息过时时，这个例程才会被调用。
 */
static void fc_vac_truncate_clog(TransactionId fc_frozenXID,
				  MultiXactId fc_minMulti,
				  TransactionId fc_lastSaneFrozenXid,
				  MultiXactId fc_lastSaneMinMulti)
{
	TransactionId fc_nextXID = ReadNextTransactionId();
	Relation	fc_relation;
	TableScanDesc fc_scan;
	HeapTuple	fc_tuple;
	Oid			fc_oldestxid_datoid;
	Oid			fc_minmulti_datoid;
	bool		fc_bogus = false;
	bool		fc_frozenAlreadyWrapped = false;

	/* 限制任务在每个集群中只有一个后端；参见 SimpleLruTruncate()。 */
	LWLockAcquire(WrapLimitsVacuumLock, LW_EXCLUSIVE);

	/* 初始化最旧的 datoids 以与我的 frozenXID/minMulti 值同步 */
	fc_oldestxid_datoid = MyDatabaseId;
	fc_minmulti_datoid = MyDatabaseId;

	/*
	 * 扫描 pg_database 以计算最小的 datfrozenxid/datminmxid
	 *
	 * 由于 vac_update_datfrozenxid 在原地更新 datfrozenxid/datminmxid，
	 * 在我们查看它们时，值可能会变化。每个值只获取一次以确保
	 * 比较逻辑的正常行为。在许多其他地方也是如此，我们假设在共享
	 * 存储中获取或更新 XID 是原子的。
	 *
	 * 注意：我们不需要担心 CREATE DATABASE 插入新条目时的竞争条件。
	 * 任何这样的条目都将包含某个现有数据库的 datfrozenxid 副本，
	 * 而该源数据库不能是我们的，因为有阻止复制包含活动后端
	 * 的数据库的互锁。因此新条目不会降低最小值。另外，如果两个 VACUUM
	 * 同时修改不同数据库的 datfrozenxid，最坏的结果是 pg_xact
	 * 不会像可能的那样被积极截断。
	 */
	fc_relation = table_open(DatabaseRelationId, AccessShareLock);

	fc_scan = table_beginscan_catalog(fc_relation, 0, NULL);

	while ((fc_tuple = heap_getnext(fc_scan, ForwardScanDirection)) != NULL)
	{
		volatile FormData_pg_database *fc_dbform = (Form_pg_database) GETSTRUCT(fc_tuple);
		TransactionId fc_datfrozenxid = fc_dbform->datfrozenxid;
		TransactionId fc_datminmxid = fc_dbform->datminmxid;

		Assert(TransactionIdIsNormal(fc_datfrozenxid));
		Assert(MultiXactIdIsValid(fc_datminmxid));

		/*
		 * 如果数据库正在被删除，或者在此过程中被
		 * 中断，则不再可能对此进行任何连接。
		 * 因此我们在这里不需要考虑它。
		 * 这很好，因为它也无法被 autovacuum 处理。
		 */
		if (database_is_invalid_form((Form_pg_database) fc_dbform))
		{
			elog(DEBUG2,
				 "skipping invalid database \"%s\" while computing relfrozenxid",
				 NameStr(fc_dbform->datname));
			continue;
		}

		/*
		 * 如果一切正常，则没有任何数据库的
		 * datfrozenxid 或 datminmxid 应该是“在未来”。但是，由于 pg_upgrade 中的错误，
		 * 这种情况是已知会发生的。如果我们看到任何“在未来”的条目，
		 * 那就退缩，不做任何事情。这确保我们不会在这些
		 * 数据库被扫描和清理之前截断 clog。（不过，如果适用，
		 * 我们将发出“已经包装”警告。）
		 */
		if (TransactionIdPrecedes(fc_lastSaneFrozenXid, fc_datfrozenxid) ||
			MultiXactIdPrecedes(fc_lastSaneMinMulti, fc_datminmxid))
			fc_bogus = true;

		if (TransactionIdPrecedes(fc_nextXID, fc_datfrozenxid))
			fc_frozenAlreadyWrapped = true;
		else if (TransactionIdPrecedes(fc_datfrozenxid, fc_frozenXID))
		{
			fc_frozenXID = fc_datfrozenxid;
			fc_oldestxid_datoid = fc_dbform->oid;
		}

		if (MultiXactIdPrecedes(fc_datminmxid, fc_minMulti))
		{
			fc_minMulti = fc_datminmxid;
			fc_minmulti_datoid = fc_dbform->oid;
		}
	}

	table_endscan(fc_scan);

	table_close(fc_relation, AccessShareLock);

	/*
	 * 如果我们似乎已经遭受了包裹溢出，则不要截断 CLOG；
	 * 计算出的最小 XID 可能是虚假的。由于 GetNewTransactionId 中的防护，
	 * 这种情况现在应该是不可能的，但我们还是保留这个测试。
	 */
	if (fc_frozenAlreadyWrapped)
	{
		ereport(WARNING,
				(errmsg("some databases have not been vacuumed in over 2 billion transactions"),
				 errdetail("You might have already suffered transaction-wraparound data loss.")));
		LWLockRelease(WrapLimitsVacuumLock);
		return;
	}

	/* 如果数据以任何其他方式是虚假的，则退缩 */
	if (fc_bogus)
	{
		LWLockRelease(WrapLimitsVacuumLock);
		return;
	}

	/*
	 * 在截断之前提前计算提交时间戳的最旧值，以便
	 * 如果用户请求一个我们正在截断的事务的时间戳，
	 * 他们会得到 NULL，而不是 slru.c 中出现的丑陋的“文件未找到”错误。
	 * 这对于 xact/multixact 来说无关紧要，因为它们不受
	 * 用户的任意查找的影响。
	 */
	AdvanceOldestCommitTsXid(fc_frozenXID);

	/*
	 * 按最旧计算值截断 CLOG、multixact 和 CommitTs。
	 */
	TruncateCLOG(fc_frozenXID, fc_oldestxid_datoid);
	TruncateCommitTs(fc_frozenXID);
	TruncateMultiXact(fc_minMulti, fc_minmulti_datoid);

	/*
	 * 更新 GetNewTransactionId 和新的 MultiXactIds 创建的包装限制。
	 * 注意：如果需要，这些函数还将向 postmaster 通知
	 * 另一个自动真空循环。 XXX 我们应该避免可能
	 * 重复发信号吗？
	 */
	SetTransactionIdLimit(fc_frozenXID, fc_oldestxid_datoid);
	SetMultiXactIdLimit(fc_minMulti, fc_minmulti_datoid, false);

	LWLockRelease(WrapLimitsVacuumLock);
}


/*
 *	vacuum_rel() -- 对一个堆关系进行真空处理
 *
 *		relid 用于标识要真空处理的关系。如果提供了关系，
 *		在报告打开/锁定关系时使用其中的名称；
 *		一旦我们成功打开了关系，就不要再使用它，因为它可能
 *		已经过时。
 *
 *		如果可以继续在该表上执行请求的 ANALYZE
 *		操作，则返回 true。
 *
 *		一次只对一个堆进行操作会产生额外的开销，因为我们需要
 *		在真空处理之前再次检查堆是否存在。我们这样做的原因
 *		是为了便于将真空处理分散到许多小事务中。
 *		否则，两阶段锁定会要求我们在真空清理器的
 *		一次传递过程中锁定整个数据库。
 *
 *		在入口和出口时，我们不在事务中。
 */
static bool fc_vacuum_rel(Oid fc_relid, RangeVar *fc_relation, VacuumParams *fc_params)
{
	LOCKMODE	fc_lmode;
	Relation	fc_rel;
	LockRelId	fc_lockrelid;
	Oid			fc_toast_relid;
	Oid			fc_save_userid;
	int			fc_save_sec_context;
	int			fc_save_nestlevel;

	Assert(fc_params != NULL);

	/* 为对该关系进行真空处理开始一个事务 */
	StartTransactionCommand();

	if (!(fc_params->options & VACOPT_FULL))
	{
		/*
		 * 在懒惰的VACUUM中，我们可以设置PROC_IN_VACUUM标志，这让
		 * 其他并发的VACUUM知道在确定它们的OldestXmin时可以忽略这个。
		 * （我们在全量VACUUM期间不设置它的原因正是因为我们可能
		 * 需要为功能索引运行用户定义的函数，我们希望确保如果它们
		 * 使用上述快照集，任何所需的元组不会从其他表中被移除。
		 * 一个依赖于其他表内容的索引函数可以说是有问题的，但我们不
		 * 会通过违反事务语义在这里破坏它。）
		 *
		 * 我们还设置了VACUUM_FOR_WRAPAROUND标志，它由
		 * autovacuum传递；用于避免取消在紧急情况下启动的VACUUM。
		 *
		 * 注意：这些标志会一直保持设置，直到CommitTransaction或
		 * AbortTransaction。我们不想在重置MyProc->xid/xmin之前清除它们，
		 * 否则GetOldestNonRemovableTransactionId()可能表现得像是回退，
		 * 这可能不好。（我们还在采取自己的快照之前设置PROC_IN_VACUUM，
		 * 这样我们的xmin不会在设置标志之前变得可见。）
		 */
		LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE);
		MyProc->statusFlags |= PROC_IN_VACUUM;
		if (fc_params->is_wraparound)
			MyProc->statusFlags |= PROC_VACUUM_FOR_WRAPAROUND;
		ProcGlobal->statusFlags[MyProc->pgxactoff] = MyProc->statusFlags;
		LWLockRelease(ProcArrayLock);
	}

	/*
	 * 需要获取快照以防止pg_subtrans被截断，
	 * 本地内存中的xids环绕，并更新xmin
	 * 边界。
	 */
	PushActiveSnapshot(GetTransactionSnapshot());

	/*
	 * 检查用户请求的中止。注意我们希望这在一个
	 * 事务内进行，以便xact.c不会发出无用的警告。
	 */
	CHECK_FOR_INTERRUPTS();

	/*
	 * 确定我们想要的锁类型——对于FULL
	 * VACUUM是硬独占锁，但对于并发VACUUM只是
	 * ShareUpdateExclusiveLock。无论哪种方式，
	 * 我们可以确信没有其他后端正在对同一表进行VACUUM。
	 */
	fc_lmode = (fc_params->options & VACOPT_FULL) ?
		AccessExclusiveLock : ShareUpdateExclusiveLock;

	/* 打开关系并获取适当的锁 */
	fc_rel = vacuum_open_relation(fc_relid, fc_relation, fc_params->options,
							   fc_params->log_min_duration >= 0, fc_lmode);

	/* 如果关系无法打开或锁定，则退出 */
	if (!fc_rel)
	{
		PopActiveSnapshot();
		CommitTransactionCommand();
		return false;
	}

	/*
	 * 检查关系是否需要根据所有权被跳过。
	 * 此检查在为手动操作构建VACUUM关系列表时也会发生，
	 * 并且在这里需要额外执行，因为VACUUM可能
	 * 在多个事务中发生，关系所有权可能已在
	 * 之间发生变化。确保在这种情况下只为VACUUM生成日志。
	 */
	if (!vacuum_is_relation_owner(RelationGetRelid(fc_rel),
								  fc_rel->rd_rel,
								  fc_params->options & VACOPT_VACUUM))
	{
		relation_close(fc_rel, fc_lmode);
		PopActiveSnapshot();
		CommitTransactionCommand();
		return false;
	}

	/*
	 * 检查它是否是可VACUUM的relkind。
	 */
	if (fc_rel->rd_rel->relkind != RELKIND_RELATION &&
		fc_rel->rd_rel->relkind != RELKIND_MATVIEW &&
		fc_rel->rd_rel->relkind != RELKIND_TOASTVALUE &&
		fc_rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
	{
		ereport(WARNING,
				(errmsg("skipping \"%s\" --- cannot vacuum non-tables or special system tables",
						RelationGetRelationName(fc_rel))));
		relation_close(fc_rel, fc_lmode);
		PopActiveSnapshot();
		CommitTransactionCommand();
		return false;
	}

	/*
	 * 不发出任何警告，默默忽略其他后端的临时表——
	 * 尝试对这些进行VACUUM会导致很大的不愉快，
	 * 因为它们的内容可能在磁盘上不是最新的。
	 * （我们在这里不抛出警告；这只会导致在全数据库
	 * VACUUM期间的无意义喧哗。）
	 */
	if (RELATION_IS_OTHER_TEMP(fc_rel))
	{
		relation_close(fc_rel, fc_lmode);
		PopActiveSnapshot();
		CommitTransactionCommand();
		return false;
	}

	/*
	 * 默默忽略分区表，因为没有工作要做。
	 * 有用的工作在它们的子分区上，这些子分区
	 * 已被单独排队等待我们处理。
	 */
	if (fc_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
	{
		relation_close(fc_rel, fc_lmode);
		PopActiveSnapshot();
		CommitTransactionCommand();
		/* 可以继续对该表进行ANALYZE */
		return true;
	}

	/*
	 * 获取会话级锁。这将保护我们在多个事务中对关系的访问，以便我们可以安全地清理关系的 TOAST 表（如果有的话），并确信没有人正在删除父关系。
	 *
	 * 注意：这不能被阻塞，即使其他人正在等待访问，因为锁管理器知道这两个锁请求来自同一个进程。
	 */
	fc_lockrelid = fc_rel->rd_lockInfo.lockRelId;
	LockRelationIdForSession(&fc_lockrelid, fc_lmode);

	/*
	 * 根据 index_cleanup reloption 设置 index_cleanup 选项，如果在 VACUUM 命令中未指定，或者在自动清理工作进程中运行时
	 */
	if (fc_params->index_cleanup == VACOPTVALUE_UNSPECIFIED)
	{
		StdRdOptIndexCleanup fc_vacuum_index_cleanup;

		if (fc_rel->rd_options == NULL)
			fc_vacuum_index_cleanup = STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO;
		else
			fc_vacuum_index_cleanup =
				((StdRdOptions *) fc_rel->rd_options)->vacuum_index_cleanup;

		if (fc_vacuum_index_cleanup == STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO)
			fc_params->index_cleanup = VACOPTVALUE_AUTO;
		else if (fc_vacuum_index_cleanup == STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON)
			fc_params->index_cleanup = VACOPTVALUE_ENABLED;
		else
		{
			Assert(fc_vacuum_index_cleanup ==
				   STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF);
			fc_params->index_cleanup = VACOPTVALUE_DISABLED;
		}
	}

	/*
	 * 根据 truncate reloption 设置 truncate 选项，如果在 VACUUM 命令中未指定，或者在自动清理工作进程中运行时
	 */
	if (fc_params->truncate == VACOPTVALUE_UNSPECIFIED)
	{
		if (fc_rel->rd_options == NULL ||
			((StdRdOptions *) fc_rel->rd_options)->vacuum_truncate)
			fc_params->truncate = VACOPTVALUE_ENABLED;
		else
			fc_params->truncate = VACOPTVALUE_DISABLED;
	}

	/*
	 * 如果调用者要求我们处理它，请记住关系的 TOAST 关系，以便稍后使用。不过，在 VACUUM FULL 中，toast 表会通过 cluster_rel 自动重建，因此我们不应该递归到它。
	 */
	if ((fc_params->options & VACOPT_PROCESS_TOAST) != 0 &&
		(fc_params->options & VACOPT_FULL) == 0)
		fc_toast_relid = fc_rel->rd_rel->reltoastrelid;
	else
		fc_toast_relid = InvalidOid;

	/*
	 * 切换到表所有者的用户 ID，使任何索引函数作为该用户运行。同时锁定安全受限操作，并安排将 GUC 变量更改限定于此命令。（对于懒惰的 VACUUM，这是不必要的，但没有害处。）
	 */
	GetUserIdAndSecContext(&fc_save_userid, &fc_save_sec_context);
	SetUserIdAndSecContext(fc_rel->rd_rel->relowner,
						   fc_save_sec_context | SECURITY_RESTRICTED_OPERATION);
	fc_save_nestlevel = NewGUCNestLevel();

	/*
	 * 执行实际工作——无论是 FULL 还是“懒惰”的清理
	 */
	if (fc_params->options & VACOPT_FULL)
	{
		ClusterParams fc_cluster_params = {0};

		/* 在清理之前关闭关系，但在提交之前保持锁定 */
		relation_close(fc_rel, NoLock);
		fc_rel = NULL;

		if ((fc_params->options & VACOPT_VERBOSE) != 0)
			fc_cluster_params.options |= CLUOPT_VERBOSE;

		/* VACUUM FULL 现在是 CLUSTER 的变体；参见 cluster.c */
		cluster_rel(fc_relid, InvalidOid, &fc_cluster_params);
	}
	else
		table_relation_vacuum(fc_rel, fc_params, vac_strategy);

	/* 回滚索引函数执行的任何 GUC 更改 */
	AtEOXact_GUC(false, fc_save_nestlevel);

	/* 恢复 userid 和安全上下文 */
	SetUserIdAndSecContext(fc_save_userid, fc_save_sec_context);

	/* 完成此类所有操作，但在提交之前保持锁定 */
	if (fc_rel)
		relation_close(fc_rel, NoLock);

	/*
	 * 完成事务并释放所有使用的临时内存。
	 */
	PopActiveSnapshot();
	CommitTransactionCommand();

	/*
	 * 如果关系有一个辅助 toast rel，在我们仍然保持对主表的会话锁定时，也进行清理。不过请注意，toast 表不会进行“分析”。这是好事，因为 toaster 总是使用硬编码的索引访问，统计信息对 toast 关系完全不重要。
	 */
	if (fc_toast_relid != InvalidOid)
		fc_vacuum_rel(fc_toast_relid, NULL, fc_params);

	/*
	 * 现在释放对主表的会话级锁。
	 */
	UnlockRelationIdForSession(&fc_lockrelid, fc_lmode);

	/* 报告我们确实完成了。 */
	return true;
}


/*
 * 打开给定关系的所有可清理索引，在每个索引上获取指定类型的锁。返回一个 Relation 指针数组到 *Irel，并返回索引数量到 *nindexes。
 *
 * 我们认为一个索引是可清理的，如果它被标记为可插入（indisready）。如果不是，则可能是 CREATE INDEX CONCURRENTLY 命令在执行早期失败，而我们所拥有的已损坏得无法处理。即使索引不是 indisvalid，我们也会进行清理；这很重要，因为在唯一索引中，无论如何都会执行唯一性检查，最好不要触碰悬空的索引指针。
 */
void vac_open_indexes(Relation fc_relation, LOCKMODE fc_lockmode,
				 int *fc_nindexes, Relation **fc_Irel)
{
	List	   *fc_indexoidlist;
	ListCell   *fc_indexoidscan;
	int			fc_i;

	Assert(fc_lockmode != NoLock);

	fc_indexoidlist = RelationGetIndexList(fc_relation);

	/* 为所有索引分配足够的内存 */
	fc_i = list_length(fc_indexoidlist);

	if (fc_i > 0)
		*fc_Irel = (Relation *) palloc(fc_i * sizeof(Relation));
	else
		*fc_Irel = NULL;

	/* 仅收集准备好的索引 */
	fc_i = 0;
	foreach(fc_indexoidscan, fc_indexoidlist)
	{
		Oid			fc_indexoid = lfirst_oid(fc_indexoidscan);
		Relation	fc_indrel;

		fc_indrel = index_open(fc_indexoid, fc_lockmode);
		if (fc_indrel->rd_index->indisready)
			(*fc_Irel)[fc_i++] = fc_indrel;
		else
			index_close(fc_indrel, fc_lockmode);
	}

	*fc_nindexes = fc_i;

	list_free(fc_indexoidlist);
}

/*
 * 释放 vac_open_indexes 获取的资源。可选择性地释放锁定（例如 NoLock 以保持它们）。
 */
void vac_close_indexes(int fc_nindexes, Relation *fc_Irel, LOCKMODE fc_lockmode)
{
	if (fc_Irel == NULL)
		return;

	while (fc_nindexes--)
	{
		Relation	fc_ind = fc_Irel[fc_nindexes];

		index_close(fc_ind, fc_lockmode);
	}
	pfree(fc_Irel);
}

/*
 * vacuum_delay_point --- 检查中断和基于成本的延迟。
 *
 * 这应该在 VACUUM 处理的每个主要循环中调用，
 * 通常每处理一页调用一次。
 */
void vacuum_delay_point(void)
{
	double		fc_msec = 0;

	/* 始终检查中断 */
	CHECK_FOR_INTERRUPTS();

	if (!VacuumCostActive || InterruptPending)
		return;

	/*
	 * 对于并行 vacuum，延迟是基于共享成本
	 * 平衡来计算的。请参见 compute_parallel_delay。
	 */
	if (VacuumSharedCostBalance != NULL)
		fc_msec = fc_compute_parallel_delay();
	else if (VacuumCostBalance >= VacuumCostLimit)
		fc_msec = VacuumCostDelay * VacuumCostBalance / VacuumCostLimit;

	/* 根据需要小憩 */
	if (fc_msec > 0)
	{
		if (fc_msec > VacuumCostDelay * 4)
			fc_msec = VacuumCostDelay * 4;

		pgstat_report_wait_start(WAIT_EVENT_VACUUM_DELAY);
		pg_usleep(fc_msec * 1000);
		pgstat_report_wait_end();

		/*
		 * 我们不希望在非常长的 vacuum
		 * 并且配置了 vacuum_cost_delay 的情况下忽略主进程的死亡。
		 * 我们不能在这里使用常规的
		 * WaitLatch() 方法，因为我们希望使用微秒为单位的睡眠
		 * 时长。
		 */
		if (IsUnderPostmaster && !PostmasterIsAlive())
			exit(1);

		VacuumCostBalance = 0;

		/* 更新工作者的平衡值 */
		AutoVacuumUpdateDelay();

		/* 在睡眠期间可能已获得中断 */
		CHECK_FOR_INTERRUPTS();
	}
}

/*
 * 计算并行工作者的 vacuum 延迟。
 *
 * 并行 vacuum 的基于成本的延迟的基本思路是允许每个
 * 工作者按其完成的工作份额进行休眠。我们通过允许所有并行
 * vacuum 工作者，包括领导进程，拥有共享的成本相关参数
 * （主要是 VacuumCostBalance）来实现这一点。
 * 我们允许每个工作者在发生任何成本时更新它，
 * 然后根据这些决定是否需要休眠。我们根据工作者所承受的成本
 * （VacuumCostBalanceLocal）计算工作者的休眠时间，
 * 然后相应地减少 VacuumSharedCostBalance。
 * 这避免了让那些 I/O 较少的工作者睡眠，从而确保
 * 进行更多 I/O 的工作者被更好地限制。
 *
 * 我们仅当工作者执行的 I/O 超过某一阈值时才允许其睡眠，
 * 该阈值是基于活动工作者的数量
 * （VacuumActiveNWorkers）计算的，并且整体成本平衡超过
 * 系统设置的 VacuumCostLimit。测试显示，如果我们强制让
 * 完成超过 50% 工作份额的工作者睡眠，就能实现所需的限制。
 */
static double fc_compute_parallel_delay(void)
{
	double		fc_msec = 0;
	uint32		fc_shared_balance;
	int			fc_nworkers;

	/* 并行 vacuum 必须处于活动状态 */
	Assert(VacuumSharedCostBalance);

	fc_nworkers = pg_atomic_read_u32(VacuumActiveNWorkers);

	/* 至少计算自身 */
	Assert(fc_nworkers >= 1);

	/* 原子性地更新共享成本平衡值 */
	fc_shared_balance = pg_atomic_add_fetch_u32(VacuumSharedCostBalance, VacuumCostBalance);

	/* 计算当前工作者的总本地平衡 */
	VacuumCostBalanceLocal += VacuumCostBalance;

	if ((fc_shared_balance >= VacuumCostLimit) &&
		(VacuumCostBalanceLocal > 0.5 * ((double) VacuumCostLimit / fc_nworkers)))
	{
		/* 根据本地成本平衡计算睡眠时间 */
		fc_msec = VacuumCostDelay * VacuumCostBalanceLocal / VacuumCostLimit;
		pg_atomic_sub_fetch_u32(VacuumSharedCostBalance, VacuumCostBalanceLocal);
		VacuumCostBalanceLocal = 0;
	}

	/*
	 * 当我们将其累积到共享值时，重置本地平衡。
	 */
	VacuumCostBalance = 0;

	return fc_msec;
}

/*
 * defGetBoolean() 的封装函数。
 *
 * 此函数返回 VACOPTVALUE_ENABLED 和 VACOPTVALUE_DISABLED，而不是
 * true 和 false。
 */
static VacOptValue
fc_get_vacoptval_from_boolean(DefElem *fc_def)
{
	return defGetBoolean(fc_def) ? VACOPTVALUE_ENABLED : VACOPTVALUE_DISABLED;
}

/*
 *	vac_bulkdel_one_index() -- 对索引关系进行批量删除。
 *
 * 返回从输入统计数据派生的批量删除统计信息
 */
IndexBulkDeleteResult * vac_bulkdel_one_index(IndexVacuumInfo *fc_ivinfo, IndexBulkDeleteResult *fc_istat,
					  VacDeadItems *fc_dead_items)
{
	/* 执行批量删除 */
	fc_istat = index_bulk_delete(fc_ivinfo, fc_istat, fc_vac_tid_reaped,
							  (void *) fc_dead_items);

	ereport(fc_ivinfo->message_level,
			(errmsg("scanned index \"%s\" to remove %d row versions",
					RelationGetRelationName(fc_ivinfo->index),
					fc_dead_items->num_items)));

	return fc_istat;
}

/*
 *	vac_cleanup_one_index() -- 对索引关系进行 vacuum 后清理。
 *
 * 返回从输入统计数据派生的批量删除统计信息
 */
IndexBulkDeleteResult * vac_cleanup_one_index(IndexVacuumInfo *fc_ivinfo, IndexBulkDeleteResult *fc_istat)
{
	fc_istat = index_vacuum_cleanup(fc_ivinfo, fc_istat);

	if (fc_istat)
		ereport(fc_ivinfo->message_level,
				(errmsg("index \"%s\" now contains %.0f row versions in %u pages",
						RelationGetRelationName(fc_ivinfo->index),
						fc_istat->num_index_tuples,
						fc_istat->num_pages),
				 errdetail("%.0f index row versions were removed.\n"
						   "%u index pages were newly deleted.\n"
						   "%u index pages are currently deleted, of which %u are currently reusable.",
						   fc_istat->tuples_removed,
						   fc_istat->pages_newly_deleted,
						   fc_istat->pages_deleted, fc_istat->pages_free)));

	return fc_istat;
}

/*
 * 返回给定 max_items 值的 VACUUM 的 dead_items 数组所需的总空间。
 */
Size vac_max_items_to_alloc_size(int fc_max_items)
{
	Assert(fc_max_items <= MAXDEADITEMS(MaxAllocSize));

	return offsetof(VacDeadItems, items) + sizeof(ItemPointerData) * fc_max_items;
}

/* 
 * vac_tid_reaped() -- 一个特定的 tid 是否可以删除? 
 * 
 * 这个函数具有作为 IndexBulkDeleteCallback 的正确签名。 
 * 
 * 假设 dead_items 数组是排序的（按升序 TID 排序）。 
 */ 
static bool fc_vac_tid_reaped(ItemPointer fc_itemptr, void *fc_state)
{
	VacDeadItems *fc_dead_items = (VacDeadItems *) fc_state;
	int64		fc_litem,
				fc_ritem,
				fc_item;
	ItemPointer fc_res;

	fc_litem = itemptr_encode(&fc_dead_items->items[0]);
	fc_ritem = itemptr_encode(&fc_dead_items->items[fc_dead_items->num_items - 1]);
	fc_item = itemptr_encode(fc_itemptr);

	/* 
 * 在 bsearch() 前做一个简单的边界检查是有用的，以避免 bsearch() 的额外开销， 
 * 特别是当堆上的死项集中在某个范围内时。由于这个函数是为每个索引元组调用的， 
 * 所以速度真的很重要。 
 */ 
	if (fc_item < fc_litem || fc_item > fc_ritem)
		return false;

	fc_res = (ItemPointer) bsearch((void *) fc_itemptr,
								(void *) fc_dead_items->items,
								fc_dead_items->num_items,
								sizeof(ItemPointerData),
								fc_vac_cmp_itemptr);

	return (fc_res != NULL);
}

/* 
 * 用于 qsort() 和 bsearch() 的比较器例程。 
 */
static int fc_vac_cmp_itemptr(const void *fc_left, const void *fc_right)
{
	BlockNumber fc_lblk,
				fc_rblk;
	OffsetNumber fc_loff,
				fc_roff;

	fc_lblk = ItemPointerGetBlockNumber((ItemPointer) fc_left);
	fc_rblk = ItemPointerGetBlockNumber((ItemPointer) fc_right);

	if (fc_lblk < fc_rblk)
		return -1;
	if (fc_lblk > fc_rblk)
		return 1;

	fc_loff = ItemPointerGetOffsetNumber((ItemPointer) fc_left);
	fc_roff = ItemPointerGetOffsetNumber((ItemPointer) fc_right);

	if (fc_loff < fc_roff)
		return -1;
	if (fc_loff > fc_roff)
		return 1;

	return 0;
}
